/*
 * SimpleAdoDB.java
 *
 */

package pl.interop.db;
import java.sql.*;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Hashtable;
import java.util.List;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.logging.Logger;
import pl.interop.db.exceptions.DBQueryException;
import pl.interop.logging.SimpleLogger;

/**
 * @version 1.01
 * @author Karol Piątek
 */
public class SimpleAdoDB {

    //pola prywatne

	/**
	 * Obiekt połączenia z bazą danych
	 */
    private Connection dbConnection;

	/**
	 * Czy ustanowiono połączenie
	 */
    private Boolean hasConnectionBeenOpened;

	/**
	 * Czy mają być wypisywane wiadomości typu debug
	 */
    private Boolean enableDebug;

	/**
	 * Czy mają być wyświetlane zapytania sql
	 * (np.: dla sterownika JDBC bazy Postgresql,
	 *  obiekt PreparedStatement zawiera w sobie informację tekstową
	 *  dotyczącą wykonanej treści zapytania, natomiast sterownik SQLite nie zawiera)
	 */
    private Boolean enableSqlDebug;

	/**
	 * Logger
	 */
    private SimpleLogger logger;

	/**
	 * Instancja SimpleAdoDB
	 */
    private static SimpleAdoDB adoDbInstance = null;


    /**
	 * Tworzy nową instancję
	 * oraz ustawia domyślny poziom debug
	 * na tryb produkcyjny
	 */
    private SimpleAdoDB() {
		hasConnectionBeenOpened = false;
		enableDebug = false;
		Logger simpleAdoDBLogger = Logger.getLogger(SimpleAdoDB.class.getName());
		logger = SimpleLogger.getInstance(simpleAdoDBLogger);
	}


	/**
	 * Pobranie referencji do obiektu typu SimpleAdoDB
	 * @return SimpleAdoDB
	 */
    public static synchronized SimpleAdoDB getInstance() {
		if(adoDbInstance == null){
			adoDbInstance = new SimpleAdoDB();
		}
		return adoDbInstance;
    }

	/**
	 * Zwraca informację, czy połączenie
	 * z bazą danych jest otwarte
	 * @return
	 * zwraca <tt>true</tt> w przypadku, gdy jest otwarte
	 * zwraca <tt>false</tt> w przypadku, gdy jest zamknięte
	 *
	 */
	public synchronized Boolean isConnected() {

		try {
			if(dbConnection != null){
				return dbConnection.isClosed();
			}
			else{
				return false;
			}
		}
		catch (SQLException ex) {
			logger.echoDev("", "", ex);
			return false;
		}
		catch (Exception ex) {
			logger.echoDev("", "", ex);
			return false;
		}
    }

	/**
	 * Przesłoniona metoda clone,
	 * w przypadku używania wzorca Singleton
	 * nie wolno dopuścić, aby ta metoda zwróciła
	 * kopię obiektu
	 *
	 * @return Object
	 * @throws CloneNotSupportedException
	 */
    @Override
    public Object clone() throws CloneNotSupportedException {
		throw new CloneNotSupportedException("Nie wolno klonować obiektu " + SimpleAdoDB.class.getSimpleName());
    }


    //metody obslugi otwierania i zamykania polaczenia


	/**
	 * Inicjalizacja sterownika
	 * @param databaseDriverName przyklad parametru :  org.postgresql.Driver
	 * @return
	 * zwraca <tt>true</tt> w przypadku poprawnego znalezienia
	 * i wczytania sterownika dostępu do bazy danych
	 */
    public synchronized Boolean initDbDriver(String databaseDriverName) {

		//inicjalizacja sterownika
		try {
			Class.forName(databaseDriverName);
			return true;
		}
		catch(java.lang.ClassNotFoundException ex) {
			logger.echoDev("","Nie znaleziono sterownika JDBC ",ex);
			return false;
		}
    }



	/**
	 * Nawiązanie połączenia z bazą danych,
	 * powinna być używana tylko w środowisku aplikacji
	 * desktop
	 * musi być używana po initDbDriver
	 * @param databaseAccessUrl
	 * @param dbLogin
	 * @param dbPassword
	 * @return
	 * zwraca <tt>true</tt> jeżeli nastąpiło poprawne połączenie
	 * zwraca <tt>false</tt>, jeżeli wystąpił błąd przy połączeniu
	 */
    public Boolean connect(String databaseAccessUrl, String dbLogin, String dbPassword) {
		try {
			dbConnection = DriverManager.getConnection(databaseAccessUrl, dbLogin, dbPassword);
			hasConnectionBeenOpened = true;
			return true;
		}
		catch(SQLException ex) {
			logger.echoDev("","Nie udało się ustanowić połączenia z bazą danych \n",ex);
			return false;
		}
    }


	/**
	 * Nawiązanie połączenia z bazą danych.
	 * Metoda preferowana w środowiskach serwerowych J2EE,
	 * gdzie źródła danych są konfigurowane z poziomu serwera
	 * @param ds - źródło danych konfigurowane na poziomie serwera i pliku WEB-INF/web.xml
	 * @return
	 * zwraca <tt>true</tt> jeżeli nastąpiło poprawne połączenie
	 * zwraca <tt>false</tt>, jeżeli wystąpił błąd przy połączeniu
	 */
    public Boolean connect(javax.sql.DataSource ds) {
        try {
            dbConnection = ds.getConnection();
            hasConnectionBeenOpened = true;
            return true;
        }
        catch(SQLException ex) {
            logger.echoDev("","Nie udało się ustanowić połączenia z bazą danych \n",ex);
            return false;
        }
    }


	/**
	 * Rozłączenie z bazą danych
	 */
    public void disconnect() {
        try {
			if(dbConnection !=null && !dbConnection.isClosed() ) {
				dbConnection.close();
				hasConnectionBeenOpened = false;
			}
        }
        catch(SQLException ex) {
            logger.echoDev("","Nie udalo sie zamknac polaczenia " + ex.getSQLState(),ex);
        }
    }

    /**
     * Przełączanie trybu automatycznego zatwierdzania
	 * zmian wykonanych poprzez np.: INSERT, DELETE, UPDATE,
     * na tryb manualny
	 * umożliwienie obsługi transakcji
     */
    public void beginTransaction() {
        try {
            dbConnection.setAutoCommit(false);
        }
        catch(SQLException ex) {
            logger.echoDev("","Nie udalo sie przełączyć trybu transkacji ",ex);
        }
    }

    /**
     *  Zatwierdzanie zmian dokonanych w transakcji
     */
    public void commitTransaction() {
        try {
            dbConnection.commit();
            dbConnection.setAutoCommit(true);
        }
        catch(SQLException ex) {
            logger.echoDev("","Nie udalo sie zatwierdzic transkacji ",ex);
        }
    }

    /**
     *  Cofanie zmian dokonanych w transakcji
     */
    public void rollbackTransaction() {
        try {
            dbConnection.rollback();
			dbConnection.setAutoCommit(true);
        }
        catch(SQLException ex) {
            logger.echoDev("","Nie udalo sie cofnac zmian w transkacji \n" + ex.getSQLState(),ex);
        }
    }

    /**
	 * Zwraca ResultSet z wykonania zapytania SELECT
	 * @param sqlQuery
	 * @param sqlQueryParams
	 * zwraca <tt>ResultSet</tt> w przypadku poprawnego wykonania zapytania
	 * @throws DBQueryException jeżeli zapytanie się nie powiedzie
	 */
    public ResultSet ExecSelect(String sqlQuery, Object...sqlQueryParams) throws DBQueryException {
        ResultSet rs = null;
        PreparedStatement prepStatement;
        try {
            prepStatement = dbConnection.prepareStatement(sqlQuery,ResultSet.TYPE_FORWARD_ONLY,ResultSet.CONCUR_READ_ONLY);
			//tutaj wykonywana jest obsluga dodawania parametrow do zapytania
            if(sqlQueryParams.length > 0) {
				for(int paramNumber=0; paramNumber < sqlQueryParams.length; paramNumber++) {
					this.addOneParam(prepStatement, paramNumber, sqlQueryParams[paramNumber]);
				}
            }
            rs = prepStatement.executeQuery();
            return rs;
        }
        catch(SQLException ex) {
            logger.echoDev("","Nie udalo sie utworzyc ResultSet \n" + ex.getSQLState(),ex);
			throw new DBQueryException(ex);
        }
    }

	/**
	 * Wykonuje prepStatement Select z przekazanymi argumentami w postaci
	 * listy dynamicznej i zwraca ResulSet
	 * @param sqlQuery
	 * @param sqlQueryParams
	 * @return
	 * zwraca <tt>ResultSet</tt> w przypadku poprawnego wykonania zapytania
	 * @throws DBQueryException jeżeli zapytanie się nie powiedzie
	 */
    public ResultSet ExecSelect(String sqlQuery, List sqlQueryParams) throws DBQueryException {
        ResultSet rs = null;
        PreparedStatement prepStatement;
        try {
            prepStatement = dbConnection.prepareStatement(sqlQuery,ResultSet.TYPE_FORWARD_ONLY,ResultSet.CONCUR_READ_ONLY);
            //tutaj wykonywana jest obsluga dodawania parametrow do zapytania
            if(!sqlQueryParams.isEmpty()) {
                int paramNumber=0;
                for(Iterator i= sqlQueryParams.iterator(); i.hasNext();) {
					Object argument = i.next();
					this.addOneParam(prepStatement, paramNumber, argument);
					paramNumber++;
                }
            }
            rs = prepStatement.executeQuery();
            return rs;
        }
        catch(SQLException ex) {
            logger.echoDev("","Nie udało się utworzyć ResultSet, SQLException \n" + ex.getSQLState(),ex);
            throw new DBQueryException(ex);
        }
        catch(Exception ex) {
            logger.echoDev("","Nie udało się utworzyć ResultSet - zwykly wyjątek \n",ex);
            return rs;
        }
    }


	/**
	 * zwraca Integer z wykonania zapytania INSERT, UPDATE lub DELETE
	 * lub innych zapytań modyfikujących strukturę bazy danych
	 * @param sqlQuery
	 * @param sqlQueryParams
	 * @return
	 *	zwraca <tt> większe lub równe 0</tt> w przypadku powodzenia wykonania zapytania
	 * @throws DBQueryException jeżeli zapytanie się nie powiedzie
	 */
    public Integer ExecUpdate(String sqlQuery, Object...sqlQueryParams) throws DBQueryException {
        Integer rs = -1;
        PreparedStatement prepStatement;
        try {
            prepStatement = dbConnection.prepareStatement(sqlQuery,ResultSet.TYPE_FORWARD_ONLY,ResultSet.CONCUR_READ_ONLY);
            //tutaj wykonywana jest obsluga dodawania parametrow do zapytania
            if(sqlQueryParams.length > 0) {
				for(int paramNumber=0; paramNumber < sqlQueryParams.length; paramNumber++) {
					this.addOneParam(prepStatement, paramNumber, sqlQueryParams[paramNumber]);
				}
            }
            rs = prepStatement.executeUpdate();
            if(rs == null)
                rs = -1;
            return rs;
        }
        catch(SQLException ex) {
            logger.echoDev("","Nie udalo sie utworzyc ResultSet \n" + ex.getSQLState(),ex);
            throw new DBQueryException(ex);
        }
    }

    /**
	 * zwraca Integer z wykonania zapytania INSERT, UPDATE lub DELETE.
	 * jezeli zwracany jest -1. znaczy ze prepStatement zostalo niepoprawnie wykonane
	 * @param sqlQuery
	 * @param sqlQueryParams
	 * @return
	 * zwraca <tt> większe lub równe 0</tt> w przypadku powodzenia wykonania zapytania
	 * @throws DBQueryException jeżeli zapytanie się nie powiedzie
	 */
    public Integer ExecUpdate(String sqlQuery, List sqlQueryParams) throws DBQueryException {

        Integer rs = -1;
        PreparedStatement prepStatement;
        try {
            prepStatement = dbConnection.prepareStatement(sqlQuery,ResultSet.TYPE_FORWARD_ONLY,ResultSet.CONCUR_READ_ONLY);
            //tutaj wykonywana jest obsluga dodawania parametrow do zapytania
            if(!sqlQueryParams.isEmpty()) {
                int paramNumber=0;
                for(Iterator i= sqlQueryParams.iterator(); i.hasNext();) {
					Object argument = i.next();
					this.addOneParam(prepStatement, paramNumber, argument);
					paramNumber++;
                }
            }
            rs = prepStatement.executeUpdate();
            if(rs == null)
                rs = -1;
            return rs;
        }
        catch(SQLException ex) {
            logger.echoDev("","Nie udalo sie utworzyc ResultSet \n" + ex.getSQLState(),ex);
            throw new DBQueryException(ex);
        }
    }

	/**
     * Pobranie z wyniku zapytania jednego wiersza jako tablicy
     * w tym przypadku nie zalezy nam na kolejnosci dodanych
     * elementow
	 * @deprecated zastąpiona przez metodę fetch
     * @param srcResultsSet
     * @return Hashtable
     */
    public Hashtable <String,Object> FetchRow(ResultSet srcResultsSet) {
        Hashtable <String,Object> oneResultRow = new Hashtable <String,Object>();
        if (srcResultsSet == null)
            return oneResultRow;

        try {
            // pobierasz metadane z ResultSet
            ResultSetMetaData rsmd = srcResultsSet.getMetaData();

            // poprzez metadane sprawdzasz
            // ile istnieje kolumn dla danego zapytania
            int columnCount = rsmd.getColumnCount();

            // iterujemy w jednym wierszu
            // przez wszystkie dostepne kolumny
            for (Integer it = 1; it <= columnCount; it++) {

				// pobiera jedna komorke, ktora chcemy wstawic
                // do zmiennej queryResults
                Object oneCell = srcResultsSet.getObject(it);

                // pobiera nazwę kolumny
                String columnLabel = rsmd.getColumnLabel(it);

                // dopisuje do listy w danej kolumnie
                // wskazaną przez nas wartość
                if(oneCell == null)
					oneCell =  new Object();

				if(this.enableDebug) {
					logger.echoDev("","Dla kolumny " + columnLabel + " wartosc " + oneCell);
					logger.echoDev("Typ danych dla wartosci " + oneCell.getClass().getName());
				}
                oneResultRow.put(columnLabel,oneCell);
            }
        }
        catch (SQLException ex) {
            logger.echoDev("","Nie udalo sie pobrac danych z  ResultSet \n" + ex.getSQLState(),ex);
        }
        return oneResultRow;
    }



    /**
     *	zwraca wynik calego zapytania w postaci tablicy
     *	klucz, i wartosci
	 *  @deprecated zastąpiona przez metodę fetch
     */
    public Hashtable <String, List<Object> > getWholeResults(ResultSet rs) {
	    // tutaj będą przechowywane queryResults
	    // z całego zapytania
	    Hashtable<String, List<Object>> queryResults = new Hashtable<String, List<Object>>();

	    if (rs == null)
		    return queryResults;
	    try {
		    // pobierasz metadane z ResultSet
		    ResultSetMetaData rsmd = rs.getMetaData();

		    // poprzez metadane sprawdzasz
		    // ile istnieje kolumn dla danego zapytania
		    int columnsCount = rsmd.getColumnCount();

		    logger.echoDev("","liczbaKolumn " + columnsCount);

		    // tutaj inicjujesz wszystkie ArrayListy,
		    // ktore beda zawarte w zmiennej queryResults

		    for (Integer it = 1; it <= columnsCount; it++) {
			    String columnLabel = rsmd.getColumnLabel(it);
			    queryResults.put(columnLabel, new ArrayList<Object>());
		    }

		    while (rs.next()) {

			    // iterujemy w jednym wierszu
			    // przez wszystkie dostepne kolumny
			    for (Integer it = 1; it <= columnsCount; it++) {
				    // pobiera jedna komorke, ktora chcemy wstawic
				    // do zmiennej queryResults
				    Object oneCell = rs.getObject(it);
				    // pobiera nazwę kolumny
				    String columnLabel = rsmd.getColumnLabel(it);
				    // dopisuje do listy w danej kolumnie
				    // wskazaną przez nas wartość
				    queryResults.get(columnLabel).add(oneCell);
			    }
		    }
		    rs.close();
		    return queryResults;
	    }
	    catch (SQLException ex) {
		    logger.echoDev("","Nie udalo sie pobrac danych z ResultSet \n" + ex.getSQLState(),ex);
	    }
	    return queryResults;
	}



	/**
	 * Służy jako ujednolicone API
	 * dla metod
	 * a) FetchRow
	 * b) getWholeResults
	 *
	 * @param srcResultsSet ResultSet z pobranymi danymi w wyniku zapytania SELECT
	 * @param transformType typ transformacji dla danych z ResultSet
	 * @return
	 * zwraca <tt>Hashtable<String, List<Object>></tt> w przypadku pobierania całego
	 * wyniku,
	 * zwraca <tt>LinkedHashMap <String,Object></tt> w przypadku pobierania jednego wiersza
	 */
	public Map fetch(ResultSet srcResultsSet, ResultSetTransformOutputEnum transformType){

		Map transformOutput = null;

		switch(transformType){
			case FETCH_ALL:
				transformOutput = Collections.synchronizedMap(new LinkedHashMap <String,List<Object> >());
				break;
			case FETCH_ROW:
			default:
				transformOutput = Collections.synchronizedMap(new LinkedHashMap <String,Object>() ); //= resultsRowPOR;
				break;
		}

        if (srcResultsSet == null)
            return transformOutput;

		try {
            // pobierasz metadane z ResultSet
            ResultSetMetaData rsmd = srcResultsSet.getMetaData();

            // poprzez metadane sprawdzasz
            // ile istnieja kolumny dla danego zapytania
            int columnCount = rsmd.getColumnCount();


			//jeżeli mamy do czynienia z pobieraniem wszystkich wyników
			if(transformType == ResultSetTransformOutputEnum.FETCH_ALL){

				Hashtable<String, List<Object>> queryResults = new Hashtable<String, List<Object>>();

				// inicjujemy wszystkie ArrayListy,
				// które będą zawarte w zmiennej queryResults
				for (Integer it = 1; it <= columnCount; it++) {
					String columnLabel = rsmd.getColumnLabel(it);
					queryResults.put(columnLabel, new ArrayList<Object>());
				}

				while (srcResultsSet.next()) {

					// iterujemy w jednym wierszu
					// przez wszystkie dostepne kolumny
					for (Integer it = 1; it <= columnCount; it++) {

						// pobieramy jedną komorkę, którą chcemy wstawić
						// do zmiennej queryResults
						Object oneCell = srcResultsSet.getObject(it);

						// pobieramy nazwę kolumny
						String columnLabel = rsmd.getColumnLabel(it);

						// dopisujemy do listy w danej kolumnie
						// wskazaną przez nas wartość
						queryResults.get(columnLabel).add(oneCell);
					}
				}
				srcResultsSet.close();
				transformOutput = queryResults;
			}
			//jeżeli mamy do czynienia z pobieraniem tylko jednego wiersza
			else{
				// iterujemy w jednym wierszu
				// przez wszystkie dostepne kolumny
				for (Integer it = 1; it <= columnCount; it++) {

					// pobieramy jedną komorkę, którą chcemy wstawić
					// do zmiennej queryResults
					Object oneCell = srcResultsSet.getObject(it);

					// pobieramy nazwę kolumny
					String columnLabel = rsmd.getColumnLabel(it);

					// dopisujemy do listy w danej kolumnie
					// wskazaną przez nas wartość
					if(oneCell == null)
						oneCell =  new Object();

					if(this.enableDebug) {
						logger.echoDev("","Dla kolumny " + columnLabel + " wartosc " + oneCell);
						logger.echoDev("Typ danych dla wartosci " + oneCell.getClass().getName());
					}
					transformOutput.put(columnLabel,oneCell);
				}
			}
        }
        catch (SQLException ex) {
            logger.echoDev("","Nie udalo sie pobrac danych z  ResultSet \n" + ex.getSQLState(),ex);
        }
		return transformOutput;
	}


    //koniec metod służących do pobierania wyników


    //obsługa dat

    /**
     * Pobieranie timestamp w postaci ciągu znaków,
     * najczęstsze użycie - pobranie aktualnej daty
     * jako sql'owy timestamp
     * DBTimeStamp(new Date().getTime())
     * @param miliSeconds
     * @return <tt>String</tt> zwraca informację jako ciąg znaków
     */
    public String DBTimeStamp(Long miliSeconds) {
		String dbTimeStamp = "";
		try {
			java.sql.Timestamp currentTime = new java.sql.Timestamp(miliSeconds);
			dbTimeStamp = currentTime.toString();
			currentTime = null;
		}
		catch(Exception ex) {
			logger.echoDev("","Error when generating sql timestamp",ex);
			java.sql.Timestamp currentTime = new java.sql.Timestamp(new java.util.Date().getTime());
			dbTimeStamp = currentTime.toString();
			currentTime = null;
		}
		return dbTimeStamp;
    }



    //koniec obsługi dat


	//metody pomocnicze służące do rozpoznawania typów danych
	//przekazywanych do zapytań SQL

	/**
	 * Wywoluje metode setObject dla obiektu PreparedStatement
     * rozpoznaje typ parametru i wywoluje odpowiednią metodę
	 * w zależności od typu przekazanego obiektu
	 * @param prepStatement
	 * @param sqlParamNumber
	 * @param sqlStatementParam
	 */
    private void addOneParam(PreparedStatement prepStatement, Integer sqlParamNumber, Object sqlQueryParamToAdd) {

		try {
			//wymagane, gdyż PreparedStatement obsługuje wyliczanie parametrów od 1
			//a nie od 0
			sqlParamNumber++;

			String sqlParamType = this.getParameterSimpleTypeName(sqlQueryParamToAdd);

			if(this.enableDebug) {
				logger.echoDev("","Typ danych dla parametru zapytania " + sqlParamType);
				logger.echoDev("","Liczba porządkowa dla parametru zapytania " + sqlParamNumber);
			}
			if(sqlParamType.equalsIgnoreCase("Timestamp")) {
				prepStatement.setTimestamp(sqlParamNumber, (Timestamp)sqlQueryParamToAdd);
			}
			else if(sqlParamType.equalsIgnoreCase("String")) {
				prepStatement.setString(sqlParamNumber, (String)sqlQueryParamToAdd);
			}
			else if(sqlParamType.equalsIgnoreCase("Integer")) {
				prepStatement.setInt(sqlParamNumber, (Integer)sqlQueryParamToAdd);
			}
			else if(sqlParamType.equalsIgnoreCase("Long")) {
				prepStatement.setLong(sqlParamNumber, (Long)sqlQueryParamToAdd);
			}
			else if(sqlParamType.equalsIgnoreCase("Float"))	{
				prepStatement.setFloat(sqlParamNumber, (Float)sqlQueryParamToAdd);
			}
			else if(sqlParamType.equalsIgnoreCase("Boolean")) {
				prepStatement.setBoolean(sqlParamNumber, (Boolean)sqlQueryParamToAdd);
			}
			else if(sqlParamType.equalsIgnoreCase("Date")) {
				prepStatement.setDate(sqlParamNumber, (Date)sqlQueryParamToAdd);
			}
        }
        catch(SQLException ex) {
            logger.echoDev("","Nie udało sie ustawić argumentu zapytania " + sqlQueryParamToAdd.toString(),ex);
        }
    }


    /**
	 * Podaje typ (klase) dla danego obiektu bez przedrostka java.lang.
	 * @param srcObject
	 * @return
	 */
    private String getParameterSimpleTypeName(Object srcObject) {
		if(this.enableDebug) {
			StringBuilder strBuilder = new StringBuilder();
			strBuilder.append("***** Simple type of object is: ");
			strBuilder.append(srcObject.getClass().getSimpleName());
			strBuilder.append(" *********** \n\n");
			logger.echoDev(strBuilder.toString());
			strBuilder = null;
		}
		return srcObject.getClass().getSimpleName();
    }

    /**
	 * Włącza lub wyłącza wewnętrzny debug
	 * dla biblioteki
	 * @param isEnabled
	 */
    public void setDebug(Boolean isEnabled) {
        enableDebug = isEnabled;
    }


	/**
	 *
	 * @param isEnabled
	 * @deprecated Metoda nieużywana, należy wykorzystać
	 * metodę setLogWriter
	 * dla DriverManager lub DataSource
	 * nie istnieje standard logowania zapytań
	 */
    public void setDebugSQL(Boolean isEnabled) {
        enableSqlDebug = isEnabled;
    }



}
